import * as d3 from 'd3';

const base = {
    Program: (node) => {
        return node.body
    },
    Statement: skipThrough,
    EmptyStatement: ignore,
    ExpressionStatement: expression,
    ParenthesizedExpression: expression,
    ChainExpression: expression,
    BlockStatement: (node) => node.body,
    IfStatement: (node) => {
        return [node.test, node.consequent, ...(node.alternate ? [node.alternate] : [])]
    },
    LabeledStatement: (node) => {
        return [node.body]
    },
    BreakStatement: ignore,
    ContinueStatement: ignore,
    WithStatement: (node) => {
        return [node.object, node.body]
    },
    SwitchStatement: (node) => {
        const res = [node.discriminant];

        for (let cs of node.cases) {
            if (cs.test) res.push(cs.test)
            for (let cons of cs.consequent)
                res.push(cons)
        }

        return res
    },
    SwitchCase: (node) => {
        const res = [];
        if (node.test) res.push(node.test)
        for (let cons of node.consequent)
            res.push(cons)
        return res;
    },
    ReturnStatement: argument,
    YieldExpression: argument,
    AwaitExpression: argument,
    ThrowStatement: argument,
    TryStatement: (node) => {
        const res = [node.block]
        if (node.handler) res.push(node.handler);
        if (node.finalizer) res.push(node.finalizer);
        return res;
    },
    CatchClause: (node) => {
        const res = []
        if (node.param) res.push(node.param);
        res.push(node.body);
        return res;
    },
    WhileStatement: (node) => {
        return [node.test, node.body]
    },
    DoWhileStatement: (node) => {
        return [node.test, node.body]
    },
    ForStatement: (node) => {
        const res = []
        if (node.init) res.push(node.init);
        if (node.test) res.push(node.test);
        if (node.update) res.push(node.update);
        res.push(node.body);
        return res;
    },
    ForInStatement: (node) => {
        return [node.left, node.right, node.body]
    },
    ForOfStatement: (node) => {
        return [node.left, node.right, node.body]
    },
    ForInit: ignore,
    DebuggerStatement: ignore,
    VariableDeclaration: (node) => {
        const res = []
        for (let decl of node.declarations)
            res.push(decl)
        return res;
    },
    VariableDeclarator: (node) => {
        const res = [node.id]
        if (node.init)
            res.push(node.init)
        return res;
    },
    Function: (node) => {
        const res = []
        if (node.id) res.push(node.id)
        for (let param of node.params)
            res.push(param)
        res.push(node.body)
        return res;
    },
    Pattern: ignore,
    VariablePattern: ignore,
    MemberPattern: ignore,
    RestElement: argument,
    ArrayPattern: (node) => {
        const res = []
        for (let elt of node.elements)
            if (elt) res.push(elt)
        return res;
    },
    ObjectPattern: (node) => {
        const res = []
        for (let prop of node.properties) {
            if (prop.type === "Property") {
                if (prop.computed) res.push(prop.key)
                res.push(prop.value)
            } else if (prop.type === "RestElement") {
                res.push(prop.argument)
            }
        }
        return res;
    },
    Expression: ignore,
    ThisExpression: ignore,
    Super: ignore,
    MetaProperty: ignore,
    ArrayExpression: (node) => {
        const res = []
        for (let elt of node.elements)
            if (elt) res.push(elt)
        return res;
    },
    ObjectExpression: (node) => {
        const res = []
        for (let prop of node.properties)
            res.push(prop)
        return res;
    },
    FunctionExpression: ignore,
    ArrowFunctionExpression: (node) => {
        const res = []
        res.push({
            type: 'params',
            virtual: true,
            v: node.params,
        })
        res.push({
            type: 'body',
            virtual: true,
            v: node.body
        })
        return res;

        // const res = []
        // for (let p of node.params)
        //     res.push(p)
        // res.push(node.body)
        // return res;
    },
    params: (node) => {
        if (Array.isArray(node.v) && node.v.length) {
            const res = []
            for (let p of node.v)
                res.push(p)
            return res;
        }
    },
    body: (node) => {
        if (node.v) {
            const res = [node.v]
            return res;
        }
    },
    FunctionDeclaration: (node) => {
        return [node.id, node.body]
    },
    SequenceExpression: (node) => {
        const res = []
        for (let expr of node.expressions)
            res.push(expr)
        return res;
    },
    TemplateLiteral: (node) => {
        const res = []
        for (let quasi of node.quasis)
            res.push(quasi)
        for (let expr of node.expressions)
            res.push(expr)
        return res;
    },
    TemplateElement: ignore,
    UnaryExpression: argument,
    BinaryExpression: (node) => [node.left, node.right],
    AssignmentExpression: (node) => [node.left, node.right],
    ConditionalExpression: (node) => [node.test, node.consequent, node.alternate],
    NewExpression: (node) => {
        const res = [node.callee]
        if (node.arguments)
            for (let arg of node.arguments)
                res.push(arg)
        return res;
    },
    MemberExpression: (node) => {
        const res = [node.object]
        res.push(node.property)
        return res;
    },
    CallExpression: (node) => {
        const res = [node.callee]
        for (let arg of node.arguments)
            res.push(arg)
        return res;
    },
    ExportNamedDeclaration: (node) => {
        const res = []
        if (node.declaration) res.push(node.declaration)
        if (node.source) res.push(node.source)
        return res;
    },
    ExportAllDeclaration: (node) => {
        const res = []
        if (node.exported) res.push(node.exported)
        res.push(node.source)
        return res;
    },
    ImportDeclaration: (node) => {
        const res = []
        for (let spec of node.specifiers)
            res.push(spec)
        res.push(node.source)
        return res;
    },
    ImportExpression: node => [node.source],
    ImportSpecifier: ignore,
    ImportDefaultSpecifier: ignore,
    ImportNamespaceSpecifier: ignore,
    Identifier: ignore,
    PrivateIdentifier: ignore,
    Literal: ignore,
    TaggedTemplateExpression: (node) => [node.tag, node.quasi],
    ClassDeclaration: ignore,
    ClassExpression: ignore,
    Class: (node) => {
        const res = []
        if (node.id) res.push(node.id)
        if (node.superClass) res.push(node.superClass)
        res.push(node.body)
        return res;
    },
    ClassBody: (node) => {
        const res = []
        for (let elt of node.body)
            res.push(elt)
        return res;
    },
    MethodDefinition: (node) => {
        const res = []
        if (node.computed) res.push(node.key)
        if (node.value) res.push(node.value)
        return res;
    },
}

function skipThrough(node) {
    return null
}

function ignore() {
    return null;
}

function expression(node) {
    return [node.expression]
}

function argument(node) { return node.argument ? [node.argument] : null }

const astChildren = (node) => {
    const { type } = node;
    return base[type] ? base[type](node) : null
}

const ext = {
    Literal: (node) => {
        return {
            ellipsis: ellipsis(node.value),
            label: node.value
        }
    },
    Identifier: (node) => {
        return {
            ellipsis: node.name,
            label: node.name,
        }
    },
    TemplateElement: (node) => {
        return {
            ellipsis: ellipsis(node.value.raw),
            label: node.value.raw,
        }
    },
    BinaryExpression: (node) => {
        return {
            ellipsis: node.operator,
            label: node.operator,
        }
    },
}

function ellipsis(text) {
    return text.length > 10 ? `${text.substring(0, 10)}...` : text
}

const astExt = (node) => {
    if (!node || !ext[node.type]) return

    return ext[node.type](node)
}

// Copyright 2021 Observable, Inc.
// Released under the ISC license.
// https://observablehq.com/@d3/tree
export default function Tree(data, { // data is either tabular (array of objects) or hierarchy (nested objects)
    path, // as an alternative to id and parentId, returns an array identifier, imputing internal nodes
    id = Array.isArray(data) ? d => d.id : null, // if tabular data, given a d in data, returns a unique identifier (string)
    parentId = Array.isArray(data) ? d => d.parentId : null, // if tabular data, given a node d, returns its parent’s identifier
    children = astChildren, // if hierarchical data, given a d in data, returns its children
    tree = d3.tree, // layout algorithm (typically d3.tree or d3.cluster)
    sort, // how to sort nodes prior to layout (e.g., (a, b) => d3.descending(a.height, b.height))
    label, // given a node d, returns the display name
    title, // given a node d, returns its hover text
    link, // given a node d, its link (if any)
    linkTarget = "_blank", // the target attribute for links (if any)
    width = 640, // outer width, in pixels
    height = 640, // outer height, in pixels
    r = 8, // radius of nodes
    fill = "#999", // fill for nodes
    fillOpacity, // fill opacity for nodes
    stroke = "#555", // stroke for links
    strokeWidth = 1.5, // stroke width for links
    strokeOpacity = 0.4, // stroke opacity for links
    strokeLinejoin, // stroke line join for links
    strokeLinecap, // stroke line cap for links
    halo = "#fff", // color of label halo 
    haloWidth = 3, // padding around the labels
} = {}) {

    // If id and parentId options are specified, or the path option, use d3.stratify
    // to convert tabular data to a hierarchy; otherwise we assume that the data is
    // specified as an object {children} with nested objects (a.k.a. the “flare.json”
    // format), and use d3.hierarchy.
    const root = path != null ? d3.stratify().path(path)(data)
        : id != null || parentId != null ? d3.stratify().id(id).parentId(parentId)(data)
            : d3.hierarchy(data, children);

    // Sort the nodes.
    if (sort != null) root.sort(sort);

    // Compute labels and titles.
    const descendants = root.descendants();
    const L = label == null ? null : descendants.map(d => label(d.data, d));

    const dx = height / 2;
    const dy = 100;
    tree().nodeSize([dx, dy])(root);

    // Center the tree.
    let x0 = Infinity;
    let x1 = -x0;
    root.each(d => {
        if (d.x > x1) x1 = d.x;
        if (d.x < x0) x0 = d.x;
    });

    // Compute the default height.
    if (height === undefined) height = x1 - x0 + dx * 2;

    const svg = d3.create("svg")
        .attr("viewBox", [-width / 2, -100, width, height])
        .attr("width", width)
        .attr("height", height)
        .attr("style", "width: 100%; height: 100%;")
        .attr("font-family", "sans-serif")
        .attr("font-size", 10)
        .call(d3.zoom().on("zoom", function (event) {
            line.attr("transform", event.transform);
            nodeG.attr("transform", event.transform);
        }));



    const line = svg.append("g")
        .attr("fill", "none")
        .attr("stroke", stroke)
        .attr("stroke-opacity", strokeOpacity)
        .attr("stroke-linecap", strokeLinecap)
        .attr("stroke-linejoin", strokeLinejoin)
        .attr("stroke-width", strokeWidth)

    line.selectAll("path")
        .data(root.links())
        .join("path")
        .attr("d", d3.linkVertical()
            .x(d => d.x)
            .y(d => d.y));

    const nodeG = svg.append("g")
    const node = nodeG.selectAll("a")
        .data(root.descendants())
        .join("a")
        .attr("xlink:href", link == null ? null : d => link(d.data, d))
        .attr("target", link == null ? null : linkTarget)
        .attr("transform", d => `translate(${d.x},${d.y})`);

    node.append("title")
        .text((d, i) => {
            const ext = astExt(d.data)
            return ext ? ext.label : null
        });

    if (L) {
        node.append("text")
            .attr("dy", "0.32em")
            .attr("text-anchor", "middle")
            .attr("paint-order", "stroke")
            .attr("stroke", halo)
            .attr("stroke-width", haloWidth)
            .text((d, i) => {
                const ext = astExt(d.data)
                if (ext) {
                    return L[i] + '：' + ext.ellipsis;
                }
                return L[i]
            })
    }

    return svg.node();
}